<!DOCTYPE html>
<meta charset=utf-8>
<title>闭合与生成器 －－ 深入 Python 3</title><!--[if IE]><script src=j/html5.js></script><![endif]-->

<link rel=stylesheet href="dip3.css">
<style>
body{counter-reset:h1 6}
</style>
<link rel=stylesheet media='only screen and (max-device-width: 480px)' href="http://woodpecker.org.cn/diveintopython3/mobile.css">
<link rel=stylesheet media=print href="http://woodpecker.org.cn/diveintopython3/print.css">
<meta name=viewport content='initial-scale=1.0'>
<form action=http://www.google.com/cse><div><input type=hidden name=cx value=014021643941856155761:l5eihuescdw><input type=hidden name=ie value=UTF-8>&nbsp;<input type=search name=q size=25 placeholder="powered by Google&trade;">&nbsp;<input type=submit name=sa value=搜索></div></form>
<p>当前位置: <a href="index.html">首页</a> <span class=u>‣</span> <a href="table-of-contents.html#generators">深入 Python 3</a> <span class=u>‣</span>
<p id=level>难度级别: <span class=u title=intermediate>♦♦♢♢♢</span>
<h1>闭合 <i class=baa>与</i> 生成器</h1>
<blockquote class=q>
<p><span class=u>❝</span> My spelling is Wobbly. It’s good spelling but it Wobbles, and the letters get in the wrong places. <span class=u>❞</span><br>— Winnie-the-Pooh
</blockquote>
<p id=toc>&nbsp;
<h2 id=divingin>深入</h2>
<p class=f>出于传递所有理解的原因，我一直对语言非常着迷。我指的不是编程语言。好吧，是编程语言，但同时也是自然语言。使用英语。英语是一种七拼八凑的语言，它从德语、法语、西班牙语和拉丁语（等等）语言中借用了大量词汇。事实上，“借用”是不恰当的词汇，“掠夺”更加符合。或者也许叫“同化“——就像博格人（译注：根据维基百科资料，Borg 是《星际旅行》虚构宇宙中的一个种族，该译法未经原作者映证）。是的，我喜欢这样。<p class=c><code>我们就是博格人。你们的语言和词源特性将会被添加到我们自己的当中。抵抗是徒劳的。</code>
<p>在本章中，将开始学习复数名词。以及返回其它函数的函数、高级正则表达式和生成器。但首先，让我们聊聊如何生成复数名词。（如果还没有阅读<a href="regular-expressions.html">《正则表达式》一章</a>，现在也许是个好时机读一读。本章将假定您理解了正则表达式的基础，并迅速进入更高级的用法。）<p>如果在讲英语的国家长大，或在正规的学校学习过英语，您可能对下面的基本规则很熟悉 ：<ul>
<li>如果某个单词以 S 、X 或 Z 结尾，添加 ES 。<i>Bass</i> 变成 <i>basses</i>， <i>fax</i> 变成 <i>faxes</i>，而 <i>waltz</i> 变成 <i>waltzes</i>。
</li><li>如果某个单词以发音的 H 结尾，加 ES；如果以不发音的 H 结尾，只需加上 S 。什么是发音的 H ？指的是它和其它字母组合在一起发出能够听到的声音。因此 <i>coach</i> 变成 <i>coaches</i> 而 <i>rash</i> 变成 <i>rashes</i>，因为在说这两个单词的时候，能够听到 CH 和 SH 的发音。但是 <i>cheetah</i> 变成 <i>cheetahs</i>，因为 H 不发音。</li><li>如果某个单词以发 I 音的字母 Y 结尾，将 Y 改成 IES；如果 Y 与某个原因字母组合发其它音的话，只需加上 S 。因此 <i>vacancy</i> 变成 <i>vacancies</i>，但 <i>day</i> 变成 <i>days</i> 。</li><li>如果所有这些规则都不适用，只需加上 S 并作最好的打算。</li></ul>
<p>（我知道，还有许多例外情况。<i>Man</i> 变成 <i>men</i> 而 <i>woman</i> 变成 <i>women</i>，但是 <i>human</i> 变成 <i>humans</i>。<i>Mouse</i> 变成 <i>mice</i> ； <i>louse</i> 变成 <i>lice</i>，但 <i>house</i> 变成 <i>houses</i>。<i>Knife</i> 变成 <i>knives</i> ；<i>wife</i> 变成 <i>wives</i>，但是 <i>lowlife</i> 变成 <i>lowlifes</i>。而且甚至我还没有开始提到那些原型和复数形式相同的单词，就像 <i>sheep</i>、 <i>deer</i> 和 <i>haiku</i>。）<p>其它语言，当然是完全不同的。<p>让我们设计一个 Python 类库用来自动进行英语名词的复数形式转换。我们将以这四条规则为起点，但要记住的不可避免地还要增加更多规则。<p class=a>⁂

<h2 id=i-know>我知道，让我们用正则表达式！</h2>
<p>因此，您正在看着单词，至少是英语单词，也就是说您正在看着字符的字符串。规则说你必须找到不同的字符组合，然后进行不同的处理。这听起来是正则表达式的工作！<p class=d>[<a href="examples/plural1.py">下载 <code>plural1.py</code></a>]<pre class=pp><code>import re

def plural(noun):          
<a>    if re.search('[sxz]$', noun):             <span class=u>①</span></a>
<a>        return re.sub('$', 'es', noun)        <span class=u>②</span></a>
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)       
    elif re.search('[^aeiou]y$', noun):      
        return re.sub('y$', 'ies', noun)     
    else:
        return noun + 's'</code></pre>
<ol>
<li>这是一条正则表达式，但它使用了在 <a href="regular-expressions.html"><i>《正则表达式》</i></a> 一章中没有讲过的语法。中括号表示“匹配这些字符的其中之一”。因此 <code>[sxz]</code> 的意思是： “<code>s</code>、 <code>x</code> 或 <code>z</code>”，但只匹配其中之一。对 <code>$</code> 应该很熟悉了，它匹配字符串的结尾。经过组合，该正则表达式将测试 <var>noun</var> 是否以 <code>s</code>、 <code>x</code> 或 <code>z</code> 结尾。</li><li>该 <code>re.sub()</code> 函数执行基于正则表达式的字符串替换。</li></ol>

<p>让我们看看正则表达式替换的细节。<pre class=screen>
<samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>import re</kbd>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.search('[abc]', 'Mark')</kbd>    <span class=u>①</span></a>
&lt;_sre.SRE_Match object at 0x001C1FA8&gt;
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.sub('[abc]', 'o', 'Mark')</kbd>  <span class=u>②</span></a>
<samp class=pp>'Mork'</samp>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.sub('[abc]', 'o', 'rock')</kbd>  <span class=u>③</span></a>
<samp class=pp>'rook'</samp>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.sub('[abc]', 'o', 'caps')</kbd>  <span class=u>④</span></a>
<samp class=pp>'oops'</samp></pre>
<ol>
<li>字符串 <code>Mark</code> 包含 <code>a</code>、 <code>b</code> 或 <code>c</code> 吗？是的，它包含 <code>a</code> 。</li><li>好了，现在查找 <code>a</code>、 <code>b</code> 或 <code>c</code>，并将其替换为 <code>o</code>。<code>Mark</code> 变成了 <code>Mork</code>。</li><li>同一函数将 <code>rock</code> 转换为 <code>rook</code> 。</li><li>您可能会认为该函数会将 <code>caps</code> 转换为 <code>oaps</code>，但实际上并是这样。<code>re.sub</code> 替换 <em>所有的</em> 匹配项，而不仅仅是第一个匹配项。因此该正则表达式将 <code>caps</code> 转换为 <code>oops</code>，因为无论是 <code>c</code> 还是 <code>a</code> 均被转换为 <code>o</code> 。</li></ol>

<p>接下来，回到 <code>plural()</code> 函数……<pre class=pp><code>def plural(noun):          
    if re.search('[sxz]$', noun):            
<a>        return re.sub('$', 'es', noun)         <span class=u>①</span></a>
<a>    elif re.search('[^aeioudgkprt]h$', noun):  <span class=u>②</span></a>
        return re.sub('$', 'es', noun)
<a>    elif re.search('[^aeiou]y$', noun):        <span class=u>③</span></a>
        return re.sub('y$', 'ies', noun)     
    else:
        return noun + 's'</code></pre>
<ol>
<li>此处将字符串的结尾（通过 <code>$</code> 匹配）替换为字符串 <code>es</code> 。换句话来说，向字符串尾部添加一个 <code>es</code> 。可以通过字符串链接来完成同样的变化，例如 <code>noun + 'es'</code>，但我对每条规则都选用正则表达式，其原因将在本章稍后更加清晰。</li><li>仔细看看，这里出现了新的变化。作为方括号中的第一个字符， <code>^</code> 有特别的含义：非。<code>[^abc]</code> 的意思是：“ <em>除了</em> <code>a</code>、 <code>b</code> 或 <code>c</code> 之外的任何字符”。因此 <code>[^aeioudgkprt]</code> 的意思是除了 <code>a</code>、 <code>e</code>、 <code>i</code>、 <code>o</code>、 <code>u</code>、 <code>d</code>、 <code>g</code>、 <code>k</code>、 <code>p</code>、<code>r</code> 或 <code>t</code> 之外的任何字符。然后该字符必须紧随一个 <code>h</code>，其后是字符串的结尾。所匹配的是以 H 结尾且 H 发音的单词。</li><li>此处有同样的模式：匹配以 Y 结尾的单词，而 Y 之前的字符 <em>不是</em> <code>a</code>、 <code>e</code>、 <code>i</code>、 <code>o</code> 或 <code>u</code>。所匹配的是以 Y 结尾，且 Y 发音听起来像 I 的单词。</li></ol>

<p>让我们看看“否定”正则表达式的更多细节。<pre class=screen>
<samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>import re</kbd>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.search('[^aeiou]y$', 'vacancy')</kbd>  <span class=u>①</span></a>
&lt;_sre.SRE_Match object at 0x001C1FA8&gt;
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.search('[^aeiou]y$', 'boy')</kbd>      <span class=u>②</span></a>
<samp class=p>&gt;&gt;&gt; </samp>
<samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.search('[^aeiou]y$', 'day')</kbd>
<samp class=p>&gt;&gt;&gt; </samp>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.search('[^aeiou]y$', 'pita')</kbd>     <span class=u>③</span></a>
<samp class=p>&gt;&gt;&gt; </samp></pre>
<ol>
<li><code>vacancy</code> 匹配该正则表达式，因为它以 <code>cy</code> 结尾，且 <code>c</code> 并非 <code>a</code>、 <code>e</code>、 <code>i</code>、 <code>o</code> 或 <code>u</code>。</li><li><code>boy</code> 不匹配，因为它以 <code>oy</code> 结尾，可以明确地说 <code>y</code> 之前的字符不能是 <code>o</code> 。<code>day</code> 不匹配，因为它以 <code>ay</code> 结尾。</li><li><code>pita</code> 不匹配，因为它不以 <code>y</code> 结尾。</li></ol>
<pre class=screen>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.sub('y$', 'ies', 'vacancy')</kbd>               <span class=u>①</span></a>
<samp class=pp>'vacancies'</samp>
<samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.sub('y$', 'ies', 'agency')</kbd>
<samp class=pp>'agencies'</samp>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>re.sub('([^aeiou])y$', r'\1ies', 'vacancy')</kbd>  <span class=u>②</span></a>
<samp class=pp>'vacancies'</samp></pre>
<ol>
<li>该正则表达式将 <code>vacancy</code> 转换为 <code>vacancies</code> ，将 <code>agency</code> 转换为 <code>agencies</code>，这正是想要的结果。注意，它也会将 <code>boy</code> 转换为 <code>boies</code>，但这永远也不会在函数中发生，因为我们首先进行了 <code>re.search</code> 以找出永远不应进行该 <code>re.sub</code> 操作的单词。</li><li>顺便，我还想指出可以将该两条正则表达式合并起来（一条查找是否应用该规则，另一条实际应用规则），使其成为一条正则表达式。它看起来是下面这个样子：其中多数内容看起来应该很熟悉：使用了在 <a href="regular-expressions.html#phonenumbers">案例研究：分析电话号码</a> 中用到的记忆分组。该分组用于保存字母 <code>y</code> 之前的字符。然后在替换字符串中，用到了新的语法： <code>\1</code>，它表示“嘿，记住的第一个分组呢？把它放到这里。”在此例中， 记住了 <code>y</code> 之前的 <code>c</code> ，在进行替换时，将用 <code>c</code> 替代 <code>c</code>，用 <code>ies</code> 替代 <code>y</code> 。（如果有超过一个的记忆分组，可以使用 <code>\2</code> 和 <code>\3</code> 等等。）</li></ol>
<p>正则表达式替换功能非常强大，而 <code>\1</code> 语法则使之愈加强大。但是，将整个操作组合成一条正则表达式也更难阅读，而且也没有直接映射到刚才所描述的复数规则。刚才所阐述的规则，像 “如果单词以 S 、X 或 Z 结尾，则添加 ES 。”如果查看该函数，有两行代码都在表述“如果以 S 、X 或 Z 结尾，那么添加 ES 。”它没有之前那种模式更直接。<p class=a>⁂

<h2 id=a-list-of-functions>函数列表</h2>

<p>现在要增加一些抽象层次的内容。我们开始时定义了一系列规则：如果这样，那样做；否则前往下一条规则。现在让我们对部分程序进行临时的复杂化，以简化另一部分。<p class=d>[<a href="examples/plural2.py">download <code>plural2.py</code></a>]
<pre class=pp><code>import re

def match_sxz(noun):
    return re.search('[sxz]$', noun)

def apply_sxz(noun):
    return re.sub('$', 'es', noun)

def match_h(noun):
    return re.search('[^aeioudgkprt]h$', noun)

def apply_h(noun):
    return re.sub('$', 'es', noun)

<a>def match_y(noun):                             <span class=u>①</span></a>
    return re.search('[^aeiou]y$', noun)
        
<a>def apply_y(noun):                             <span class=u>②</span></a>
    return re.sub('y$', 'ies', noun)

def match_default(noun):
    return True

def apply_default(noun):
    return noun + 's'

<a>rules = ((match_sxz, apply_sxz),               <span class=u>③</span></a>
         (match_h, apply_h),
         (match_y, apply_y),
         (match_default, apply_default)
         )

def plural(noun):           
<a>    for matches_rule, apply_rule in rules:       <span class=u>④</span></a>
        if matches_rule(noun):
            return apply_rule(noun)</code></pre>
<ol>
<li>现在，每条匹配规则都有自己的函数，它们返回对 <code>re.search()</code> 函数调用结果。</li><li>每条应用规则也都有自己的函数，它们调用 <code>re.sub()</code> 函数以应用恰当的复数变化规则。</li><li>现在有了一个 <code>rules</code> 数据结构——一个函数对的序列，而不是一个函数（<code>plural()</code>）实现多个条规则。</li><li>由于所有的规则被分割成单独的数据结构，新的 <code>plural()</code> 函数可以减少到几行代码。使用 <code>for</code> 循环，可以一次性从 <var>rules</var> 这个数据结构中取出匹配规则和应用规则这两样东西（一条匹配对应一条应用）。在 <code>for</code> 循环的第一次迭代过程中， <var>matches_rule</var> 将获取 <code>match_sxz</code>，而 <var>apply_rule</var> 将获取 <code>apply_sxz</code>。在第二次迭代中（假定可以进行到这一步）， <var>matches_rule</var> 将会赋值为 <code>match_h</code>，而 <var>apply_rule</var> 将会赋值为 <code>apply_h</code> 。该函数确保最终能够返回某个值，因为终极匹配规则 (<code>match_default</code>) 只返回 <code>True</code>，意思是对应的应用规则 (<code>apply_default</code>) 将总是被应用。</li></ol>

<aside>变量 “rules” 是一系列函数对。</aside>
<p>该技术能够成功运作的原因是 <a href="your-first-python-program.html#everythingisanobject">Python 中一切都是对象</a>，包括了函数。数据结构 <var>rules</var> 包含了函数——不是函数的名称，而是实际的函数对象。在 <code>for</code> 循环中被赋值后，<var>matches_rule</var> 和 <var>apply_rule</var> 是可实际调用的函数。在第一次 <code>for</code> 循环的迭代过程中，这相当于调用 <code>matches_sxz(noun)</code>，如果返回一个匹配值，将调用 <code>apply_sxz(noun)</code> 。<p>如果这种附加抽象层令你迷惑，可以试着展开函数以了解其等价形式。整个 <code>for</code> 循环等价于下列代码：<pre class='nd pp'><code>
def plural(noun):
    if match_sxz(noun):
        return apply_sxz(noun)
    if match_h(noun):
        return apply_h(noun)
    if match_y(noun):
        return apply_y(noun)
    if match_default(noun):
        return apply_default(noun)</code></pre>

<p>这段代码的好处是 <code>plural()</code> 函数被简化了。它处理一系列其它地方定义的规则，并以通用的方式对它们进行迭代。<ol>
<li>获取某匹配规则</li><li>是否匹配？然后调用应用规则，并返回结果。</li><li>不匹配？返回步骤 1 。</li></ol>

<p>这些规则可在任何地方以任何方式定义。<code>plural()</code> 函数并不关心。<p>现在，新增的抽象层是否值得呢？嗯，还没有。让我们考虑下要向函数中新增一条规则时该如何操作。在第一例中，将需要新增一条 <code>if</code> 语句到 <code>plural()</code> 函数中。在第二例中，将需要新增两个函数， <code>match_foo()</code> 和 <code>apply_foo()</code>，然后更新 <var>rules</var> 序列以指定新的匹配和应用函数按照其它规则按顺序调用。<p>但是对于下一节来说，这只是一个跳板而已。让我们继续……<p class=a>⁂

<h2 id=a-list-of-patterns>匹配模式列表</h2>

<p>其实并不是真的有必要为每个匹配和应用规则定义各自的命名函数。它们从未直接被调用，而只是被添加到 <var>rules</var> 序列并从该处被调用。此外，每个函数遵循两种模式的其中之一。所有的匹配函数调用 <code>re.search()</code>，而所有的应用函数调用 <code>re.sub()</code>。让我们将模式排除在考虑因素之外，使新规则定义更加简单。<p class=d>[<a href="examples/plural3.py">download <code>plural3.py</code></a>]
<pre class=pp><code>import re

def build_match_and_apply_functions(pattern, search, replace):
<a>    def matches_rule(word):                                     <span class=u>①</span></a>
        return re.search(pattern, word)
<a>    def apply_rule(word):                                       <span class=u>②</span></a>
        return re.sub(search, replace, word)
<a>    return (matches_rule, apply_rule)                           <span class=u>③</span></a></code></pre>
<ol>
<li><code>build_match_and_apply_functions()</code> 函数用于动态创建其它函数。它接受 <var>pattern</var>、 <var>search</var> 和 <var>replace</var> 三个参数，并定义了 <code>matches_rule()</code> 函数，该函数通过传给 <code>build_match_and_apply_functions()</code> 函数的 <var>pattern</var> 及传递给所创建的 <code>matchs_rules()</code> 函数的 <var>word</var> 调用 <code>re.search()</code> 函数，哇。</li><li>应用函数的创建工作采用了同样的方式。应用函数只接受一个参数，并使用传递给 <code>build_match_and_apply_functions()</code> 函数的 <var>search</var> 和 <var>replace</var> 参数、以及传递给要创建 <code>apply_rule()</code> 函数的 <var>word</var> 调用 <code>re.sub()</code>。在动态函数中使用外部参数值的技术称为 <em>闭合【closures】</em>。基本上，常量的创建工作都在创建应用函数过程中完成：它接受一个参数 （<var>word</var>），但实际操作还加上了另外两个值（<var>search</var> 和 <var>replace</var>），该两个值都在定义应用函数时进行设置。</li><li>最后，<code>build_match_and_apply_functions()</code> 函数返回一个包含两个值的元组：即刚才所创建的两个函数。在这些函数中定义的常量（ <code>match_rule()</code> 函数中的 <var>pattern</var> 函数，<code>apply_rule()</code> 函数中的 <var>search</var> 和 <var>replace</var> ）与这些函数呆在一起，即便是在从 <code>build_match_and_apply_functions()</code> 中返回后也一样。这真是非常酷的一件事情。</li></ol>

<p>但如果此方式导致了难以置信的混乱（应该是这样，它确实有点奇怪），在看看如何使用之后可能会清晰一些。<pre class=pp><code><a>patterns = \                                                        <span class=u>①</span></a>
  (
    ('[sxz]$',           '$',  'es'),
    ('[^aeioudgkprt]h$', '$',  'es'),
    ('(qu|[^aeiou])y$',  'y$', 'ies'),
<a>    ('$',                '$',  's')                                 <span class=u>②</span></a>
  )
<a>rules = [build_match_and_apply_functions(pattern, search, replace)  <span class=u>③</span></a>
         for (pattern, search, replace) in patterns]</code></pre>
<ol>
<li>我们的复数形式“规则”现在被定义为 <em>字符串</em> 的元组的元组（而不是函数）。每个组的第一个字符串是在 <code>re.search()</code> 中用于判断该规则是否匹配的正则表达式。各组中的第二和第三个字符串是在 <code>re.sub()</code> 中将实际用于使用规则将名词转换为复数形式的搜索和替换表达式。</li><li>此处的后备规则略有变化。在前例中，<code>match_default()</code> 函数仅返回 <code>True</code>，意思是如果更多的指定规则无一匹配，代码将简单地向给定词汇的尾部添加一个 <code>s</code>。本例则进行了一些功能等同的操作。最后的正则表达式询问单词是否有一个结尾（<code>$</code> 匹配字符串的结尾）。当然，每个字符串都有一个结尾，甚至是空字符串也有，因此该规则将始终被匹配。因此，它实现了 <code>match_default()</code> 函数同样的目的，始终返回 <code>True</code>：它确保了如果没有更多的指定规则用于匹配，代码将向给定单词的尾部增加一个 <code>s</code> 。</li><li>本行代码非常神奇。它以 <var>patterns</var> 中的字符串序列为参数，并将其转换为一个函数序列。怎么做到的？通过将字符串“映射”到 <code>build_match_and_apply_functions()</code> 函数。也就是说，它接受每组三重字符串为参数，并将该三个字符串作为实参调用 <code>build_match_and_apply_functions()</code> 函数。 <code>build_match_and_apply_functions()</code> 函数返回一个包含两个函数的元组。也就是说该 <var>规则</var> 最后的结尾与前例在功能上是等价的：一个元组列表，每个元组都是一对函数。第一个函数是调用 <code>re.search()</code> 的匹配函数；而第二个函数调用 <code>re.sub()</code> 的应用函数。</li></ol>

<p>此版本脚本的最前面是主入口点—— <code>plural()</code> 函数。<pre class=pp><code>def plural(noun):
<a>    for matches_rule, apply_rule in rules:  <span class=u>①</span></a>
        if matches_rule(noun):
            return apply_rule(noun)</code></pre>
<ol>
<li>由于 <var>规则</var> 列表与前例中的一样（实际上确实相同），因此毫不奇怪 <code>plural()</code> 函数基本没有发生变化。它是完全通用的，它以规则函数列表为参数，并按照顺序调用它们。它并不关系规则是如何定义的。在前例中，它们被定义为各自命名的函数。现在它们通过将 <code>build_match_and_apply_functions()</code> 函数的输出映射为源字符串的列表来动态创建。这没有任何关系； <code>plural()</code> 函数将以同样方式运作。</li></ol>

<p class=a>⁂

<h2 id=a-file-of-patterns>匹配模式文件</h2>

<p>目前，已经排除了重复代码，增加了足够的抽象性，因此复数形式规则可以字符串列表的形式进行定义。下一个逻辑步骤是将这些字符串放入一个单独的文件中，因此可独立于使用它们的代码来进行维护。<p>首先，让我们创建一份包含所需规则的文本文件。没有花哨的数据结构，只有空白符分隔的三列字符串。将其命名为 <code>plural4-rules.txt</code>.<p class=d>[<a href="examples/plural4-rules.txt">download <code>plural4-rules.txt</code></a>]
<pre class='nd pp'><code>[sxz]$               $    es
[^aeioudgkprt]h$     $    es
[^aeiou]y$          y$    ies
$                    $    s</code></pre>

<p>下面看看如何使用该规则文件。<p class=d>[<a href="examples/plural4.py">download <code>plural4.py</code></a>]
<pre class=pp><code>import re

<a>def build_match_and_apply_functions(pattern, search, replace):  <span class=u>①</span></a>
    def matches_rule(word):
        return re.search(pattern, word)
    def apply_rule(word):
        return re.sub(search, replace, word)
    return (matches_rule, apply_rule)

rules = []
<a>with open('plural4-rules.txt', encoding='utf-8') as pattern_file:  <span class=u>②</span></a>
<a>    for line in pattern_file:                                      <span class=u>③</span></a>
<a>        pattern, search, replace = line.split(None, 3)             <span class=u>④</span></a>
<a>        rules.append(build_match_and_apply_functions(              <span class=u>⑤</span></a>
                pattern, search, replace))</code></pre>
<ol>
<li><code>build_match_and_apply_functions()</code> 函数没有发生变化。仍然使用了闭合技术：通过外部函数中定义的变量来动态创建两个函数。</li><li>全局的 <code>open()</code> 函数打开文件并返回一个文件对象。此例中，将要打开的文件包含了名词复数形式的模式字符串。<code>with</code> 语句创建了叫做 <i>context【上下文】</i>的东西：当 <code>with</code> 块结束时，Python 将自动关闭文件，即便是在 <code>with</code> 块中引发了例外也会这样。在 <a href="files.html">《文件》</a> 一章中将学到关于 <code>with</code> 块和文件对象的更多内容。</li><li><code>for line in &lt;fileobject&gt;</code> 代码从打开的文件中读取数据，并将文本赋值给 <var>line</var> 变量。在 <a href="files.html">《文件》</a> 一章中将学到更多关于读取文件的内容。</li><li>文件中每行都有三个值，单它们通过空白分隔（制表符或空白，没有区别）。要将它们分开，可使用字符串方法 <code>split()</code> 。<code>split()</code> 方法的第一个参数是 <code>None</code>，表示“对任何空白字符进行分隔（制表符或空白，没有区别）”。第二个参数是 <code>3</code>，意思是“针对空白分隔三次，丢弃该行剩下的部分。”像 <code>[sxz]$ $ es</code> 这样的行将被分割为列表 <code>['[sxz]$', '$', 'es']</code>，意思是 <var>pattern</var> 获得值 <code>'[sxz]$'</code>， <var>search</var> 获得值 <code>'$'</code>，而 <var>replace</var> 获得值 <code>'es'</code>。对于短短的一行代码来说确实威力够大的。</li><li>最后，将 <code>pattern</code> 、 <code>search</code> 和 <code>replace</code> 传入 <code>build_match_and_apply_functions()</code> 函数，它将返回一个函数的元组。将该元组添加到 <var>rules</var> 列表，最终 <var>rules</var> 将储存 <code>plural()</code> 函数所预期的匹配和应用函数列表。</li></ol>

<p>此处的改进是将复数形式规则独立地放到了一份外部文件中，因此可独立于使用它的代码单独对规则进行维护。代码是代码，数据是数据，生活更美好。<p class=a>⁂

<h2 id=generators>生成器</h2>

<p>如果有个通用 <code>plural()</code> 函数解析规则文件不就更棒了吗？获取规则，检查匹配，应用相应的转换，进入下一条规则。这是 <code>plural()</code> 函数所必须完成的事，也是 <code>plural()</code> 函数必须做的事。<p class=d>[<a href="examples/plural5.py">download <code>plural5.py</code></a>]
<pre class='nd pp'><code>def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 3)
            yield build_match_and_apply_functions(pattern, search, replace)

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))</code></pre>

<p><em>这段</em>代码到底是如何运作的？让我们先看一个交互式例子。<pre class=screen>
<samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>def make_counter(x):</kbd>
<samp class=p>... </samp><kbd class=pp>    print('entering make_counter')</kbd>
<samp class=p>... </samp><kbd class=pp>    while True:</kbd>
<a><samp class=p>... </samp><kbd class=pp>        yield x</kbd>                    <span class=u>①</span></a>
<samp class=p>... </samp><kbd class=pp>        print('incrementing x')</kbd>
<samp class=p>... </samp><kbd class=pp>        x = x + 1</kbd>
<samp class=p>... </samp>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>counter = make_counter(2)</kbd>          <span class=u>②</span></a>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>counter</kbd>                            <span class=u>③</span></a>
&lt;generator object at 0x001C9C10&gt;
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>next(counter)</kbd>                      <span class=u>④</span></a>
<samp>entering make_counter
2</samp>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>next(counter)</kbd>                      <span class=u>⑤</span></a>
<samp>incrementing x
3</samp>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>next(counter)</kbd>                      <span class=u>⑥</span></a>
<samp>incrementing x
4</samp></pre>
<ol>
<li><code>make_counter</code> 中出现的 <code>yield</code> 命令的意思是这不是一个普通的函数。它是一次生成一个值的特殊类型函数。可以将其视为可恢复函数。调用该函数将返回一个可用于生成连续 <var>x</var> 值的 <i>生成器【Generator】</i>。</li><li>为创建 <code>make_counter</code> 生成器的实例，仅需像调用其它函数那样对它进行调用。注意该调用并不实际执行函数代码。可以这么说，是因为 <code>make_counter()</code> 函数的第一行调用了 <code>print()</code>，但实际并未打印任何内容。</li><li>该 <code>make_counter()</code> 函数返回了一个生成器对象。</li><li><code>next()</code> 函数以一个生成器对象为参数，并返回其下一个值。对 <var>counter</var> 生成器第一次调用 <code>next()</code> ，它针对第一条 <code>yield</code> 语句执行 <code>make_counter()</code> 中的代码，然后返回所产生的值。在此情况下，该代码输出将为 <code>2</code>，因其仅通过调用 <code>make_counter(2)</code> 对生成器进行初始创建。</li><li>对同一生成器对象反复调用 <code>next()</code> 将确切地从上次调用的位置开始继续，直到下一条 <code>yield</code> 语句。所有的变量、局部数据等内容在 <code>yield</code> 时被保存，在 <code>next()</code> 时被恢复。下一行代码等待被执行以调用 <code>print()</code> 以打印出 <samp>incrementing x</samp> 。之后，执行语句 <code>x = x + 1</code>。然后它继续通过 <code>while</code> 再次循环，而它再次遇上的第一条语句是 <code>yield x</code>，该语句将保存所有一切状态，并返回当前 <var>x</var> 的值（当前为 <code>3</code>）。</li><li>第二次调用 <code>next(counter)</code> 时，又进行了同样的工作，但这次 <var>x</var> 为 <code>4</code>。</li></ol>

<p>由于 <code>make_counter</code> 设置了一个无限循环，理论上可以永远执行该过程，它将不断递增 <var>x</var> 并输出数值。还是让我们看一个更加实用的生成器用法。<h3 id=a-fibonacci-generator>斐波那奇生成器</h3>

<aside>“yield” 暂停一个函数。“next()” 从其暂停处恢复其运行。</aside>

<p class=d>[<a href="examples/fibonacci.py">download <code>fibonacci.py</code></a>]
<pre class=pp><code>def fib(max):
<a>    a, b = 0, 1          <span class=u>①</span></a>
    while a &lt; max:
<a>        yield a          <span class=u>②</span></a>
<a>        a, b = b, a + b  <span class=u>③</span></a></code></pre>
<ol>
<li>斐波那契序列是一系列的数字，每个数字都是其前两个数字之和。它从 0 和 <code>1</code> 开始，初始时上升缓慢，但越来越快。启动该序列需要两个变量：从 0 开始的 <var>a</var>，和从 <code>1</code> 开始的 <var>b</var> 。</li><li><var>a</var> 是当前序列中的数字，因此对它进行 yield 操作。</li><li><var>b</var> 是序列中下一个数字，因此将它赋值给 <var>a</var>，但同时计算下一个值 (<code>a + b</code>) 并将其赋值给 <var>b</var> 以供稍后使用。注意该步骤是并行发生的；如果 <var>a</var> 为 <code>3</code> 且 <var>b</var> 为 <code>5</code>，那么 <code>a, b = b, a + b</code> 将会把 <var>a</var> 设置 <code>5</code> （<var>b</var> 之前的值），将 <var>b</var> 设置为 <code>8</code> （ <var>a</var> 和 <var>b</var> 之前值的和）。</li></ol>

<p>因此，现在有了一个连续输出斐波那契数值的函数。当然，还可以使用递归来完成该功能，但这个方式更易于阅读。同样，它也与 <code>for</code> 循环合作良好。<pre class=screen>
<samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>from fibonacci import fib</kbd>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>for n in fib(1000):</kbd>      <span class=u>①</span></a>
<a><samp class=p>... </samp><kbd class=pp>    print(n, end=' ')</kbd>    <span class=u>②</span></a>
<samp class=pp>0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987</samp>
<a><samp class=p>&gt;&gt;&gt; </samp><kbd class=pp>list(fib(1000))</kbd>          <span class=u>③</span></a>
<samp class=pp>[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]</samp></pre>
<ol>
<li>可以在 <code>for</code> 循环中直接使用像 <code>fib()</code> 这样的生成器。<code>for</code> 循环将会自动调用 <code>next()</code> 函数，从 <code>fib()</code> 生成器获取数值并赋值给 <code>for</code> 循环索引变量。（<var>n</var>）</li><li>每经过一次 <code>for</code> 循环， <var>n</var> 从 <code>fib()</code> 的 <code>yield</code> 语句获取一个新值，所需做的仅仅是输出它。一旦 <code>fib()</code> 的数字用尽（<var>a</var> 大于 <var>max</var>，即本例中的 <code>1000</code>）， <code>for</code> 循环将会自动退出。</li><li>这是一个很有用的用法：将一个生成器传递给 <code>list()</code> 函数，它将遍历整个生成器（就像前例中的 <code>for</code> 循环）并返回所有数值的列表。</li></ol>

<h3 id=a-plural-rule-generator>复数规则生成器</h3>

<p>让我们回到 <code>plural5.py</code> 看看该版本的 <code>plural()</code> 函数是如何运作的。<pre class=pp><code>def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
<a>            pattern, search, replace = line.split(None, 3)                   <span class=u>①</span></a>
<a>            yield build_match_and_apply_functions(pattern, search, replace)  <span class=u>②</span></a>

def plural(noun, rules_filename='plural5-rules.txt'):
<a>    for matches_rule, apply_rule in rules(rules_filename):                   <span class=u>③</span></a>
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))</code></pre>
<ol>
<li>此处没有太神奇的代码。由于规则文件中每行都靠包括以空白相间的三个值，因此使用 <code>line.split(None, 3)</code> 获取三个“列”的值并将它们赋值给三个局部变量。</li><li><em>然后使用了 yield。</em> 但生产了什么呢？通过老朋友—— <code>build_match_and_apply_functions()</code> 动态创建的两个函数，这与之前的例子是一样的。换而言之， <code>rules()</code> 是<em>按照需求</em>连续生成匹配和应用函数的生成器。</li><li>由于 <code>rules()</code> 是生成器，可直接在 <code>for</code> 循环中使用它。对 <code>for</code> 循环的第一次遍历，可以调用 <code>rules()</code> 函数打开模式文件，读取第一行，从该行的模式动态创建一个匹配函数和应用函数，然后生成动态创建的函数。对 <code>for</code> 循环的第二次遍历，将会精确地回到 <code>rules()</code> 中上次离开的位置（在 <code>for line in pattern_file</code> 循环的中间）。要进行的第一项工作是读取文件（仍处于打开状态）的下一行，基于该行的模式动态创建另一匹配和应用函数，然后生成两个函数。</li></ol>

<p>通过第四步获得了什么呢？启动时间。在第四步中引入 <code>plural4</code> 模块时，它读取了整个模式文件，并创建了一份所有可能规则的列表，甚至在考虑调用 <code>plural()</code> 函数之前。有了生成器，可以轻松地处理所有工作：可以读取规则，创建函数并试用它们，如果该规则可用甚至可以不读取文件剩下的部分或创建更多的函数。<p>失去了什么？性能！每次调用 <code>plural()</code> 函数，<code>rules()</code> 生成器将从头开始——这意味着重新打开模式文件，并从头开始读取，每次一行。<p>要是能够两全其美多好啊：最低的启动成本（无需对 <code>import</code> 执行任何代码），<em>同时</em> 最佳的性能（无需一次次地创建同一函数）。哦，还需将规则保存在单独的文件中（因为代码和数据要泾渭分明），还有就是永远不必两次读取同一行。<p>要实现该目标，必须建立自己的生成器。在进行<em>此工作</em>之前，必须对 Python 的类进行学习。<p class=a>⁂

<h2 id=furtherreading>深入阅读</h2>
<ul>
<li><a href=http://www.python.org/dev/peps/pep-0255/>PEP 255: 简单生成器</a>
</li><li><a href=http://effbot.org/zone/python-with-statement.htm>理解 Python 的 “with” 语句</a>
</li><li><a href=http://ynniv.com/blog/2007/08/closures-in-python.html>Python 中的闭合</a>
</li><li><a href=http://en.wikipedia.org/wiki/Fibonacci_number>斐波那契数值</a>
</li><li><a href=http://www2.gsu.edu/~wwwesl/egw/crump.htm>英语的不规则复数名词</a>
</li></ul>

<p class=v><a href="regular-expressions.html" rel=prev title="back to &#8220;Regular Expressions&#8221;"><span class=u>☜</span></a> <a href="iterators.html" rel=next title="onward to &#8220;Classes &amp; Iterators&#8221;"><span class=u>☞</span></a>

<p class=c>© 2001–9 <a href="about.html">Mark Pilgrim</a>
<script src="j/jquery.js"></script>
<script src="j/prettify.js"></script>
<script src="j/dip3.js"></script>
